package com.jb.framework.pm;

import com.jb.faces.component.UIComponent;
import com.jb.framework.DuplicateComponentException;
import com.jb.framework.Persistable;
import com.jb.framework.PersistentManager;
import com.jb.framework.util.PropUtil;
import com.jb.framework.util.XMLUtil;
import com.jb.util.ClassUtil;
import com.jb.util.Meter;
import com.jb.web.application.configuration.Config;
import com.jb.web.bean.Page;
import com.jb.web.bean.UIComponentImpl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 * XML Persistent manager for beans. Save beans to a file system.
 *
 * @author twang
 * @version 1.0
 */
public class XMLPersistentManager implements PersistentManager {
    public static int READ = 0;
    public static int WRITE = 1;
    public static int DELETE = 2;
    public static int CREATE = 3;
    public static final String EXT = ".xsp";
    HashMap cache = new HashMap();
    PersistentManager mParentPersistentManager;
    String location;
    boolean readOnly = false;
    private Log log = LogFactory.getLog(getClass().getName());

    public XMLPersistentManager() {
    }

    public XMLPersistentManager(String location) {
        mParentPersistentManager = null;
        this.location = location;
    }

    public XMLPersistentManager(PersistentManager pParentLoader, String location) {
        mParentPersistentManager = pParentLoader;
        this.location = location;
    }

    /**
     * Sets property value to a Persistable
     *
     * @param comp
     * @param propName property name.
     * @param propValue serialized property value.
     */
    private void setComponentProperty(Persistable comp, String propName, Object propValue) {
        if (propValue instanceof String) {
            propValue = XMLUtil.decodeXMLSafe("" + propValue);
        }
        PropUtil.setProperty(comp, propName, propValue);
    }

    /**
     * Returns whether if this persistent manager is read only or not.
     *
     * @return true if this is a read only class.
     */
    public boolean isReadOnly() {
        return readOnly;
    }

    /**
     * Sets the root location for this manager.
     *
     * @param location the root location for this manager.
     */
    public void setTopLocation(String location) {
        this.location = location;
    }

    /**
     * Save the object to a file.
     *
     * @param object to be saved
     *
     * @throws DuplicateComponentException thrown if a file already exists. This
     * probably means an object with the same id and type already exists in
     * the system.
     */
    public void create(Persistable object) throws DuplicateComponentException {
        if (!checkSecurity(object, CREATE)) {
            return;
        }

        String classPath = object.getClass().getName();
        classPath = classPath.replace('.', File.separatorChar);

        String fileName = getLocation(object) + File.separator + classPath + File.separator + getFileName(object) +
            EXT;
        File file = new File(fileName);

        if (file.exists()) {
            throw new DuplicateComponentException(object.getId());
        }

        save(object);
    }
 
    /**
     * Deletes a bean
     *
     * @param root DOCUMENT ME!
     */
    public void delete(Persistable root) {
        if (!checkSecurity(root, DELETE)) {
            return;
        }

        String classPath = root.getClass().getName();

        cache.remove(location + classPath + root.getId());

        classPath = classPath.replace('.', File.separatorChar);

        String dirName = getLocation(root) + File.separator + classPath;
        String fileName = dirName + File.separator + getFileName(root) + EXT;
        File file = new File(fileName);

        if (file.exists()) {
            String bakFileName = dirName + File.separatorChar + getFileName(root) + ".bak";

            File backFile = new File(bakFileName);
            boolean bValue = false;

            if (backFile.exists()) {
                bValue = backFile.delete();

                if (!bValue) {
                    throw new RuntimeException("Failed to delete the back up file");
                }
            }

            bValue = file.renameTo(backFile);

            if (!bValue) {
                throw new RuntimeException("Failed to delete the file");
            }
        }
    }

    /**
     * Find object by its class and id. First it look into its cache, if not
     * found, it will load from a file system.
     *
     * @param childClass type of object to be found
     * @param id object id to be found
     *
     * @return object found, null if not found.
     */
    public Object findById(Class childClass, Object id) {
        try {
            Meter.start();
            Persistable beanz = (Persistable) cache.get(location + childClass.getName() + id);

            if (beanz != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Hit cache " + childClass.getName() + ":" + id);
                }
                return beanz;
            }

            beanz = (Persistable) childClass.newInstance();
            beanz.setId(id.toString());

            if (load(beanz)) {
                if (log.isDebugEnabled()) {
                    log.debug("Cache " + childClass.getName() + ":" + id);
                }

                return beanz;
            } else {
                return null; //not found
            }
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error(this, e);
            }

            return null; //not found
        }finally{
            Meter.stop(this.getClass().getName() + " findById " + id);
        }
    }

    /**
     * Loads a persistable from file. This call does not use
     * cached object. It will reload the object. Update the cache when
     * loaded successfully.
     *
     * @param root to be loaded
     *
     * @return true if succeed, or false.
     */
    public boolean load(Persistable root) {
        if (!checkSecurity(root, READ)) {
            return false;
        }

        boolean loaded = false;
        String classPath = root.getClass().getName();
        classPath = classPath.replace('.', File.separatorChar);

        String dirName = getLocation(root) + File.separatorChar + classPath;
        String fileName = dirName + File.separatorChar + getFileName(root);

        if (!fileName.endsWith(EXT)) {
            fileName += EXT;
        }

        loaded = load(root, fileName);

        if (!loaded) {
            if (mParentPersistentManager != null) {
                loaded = mParentPersistentManager.load(root);
            }
        } else {
            if (root instanceof Page) {
                ((Page) root).setLoader(this);
            }

            cache.put(location + root.getClass().getName() + root.getId(), root);
        }
        return loaded;
    } 
    /**
     * Save bean
     * @param root
     * @param saveChildren
     */
    public void save(Persistable root, boolean saveChildren) {
        if (!checkSecurity(root, WRITE)) {
            return;
        }

        String classPath = root.getClass().getName();
        classPath = classPath.replace('.', File.separatorChar);

        String dirName = getLocation(root) + File.separatorChar + classPath;
        String fileName = dirName + File.separatorChar + getFileName(root) + EXT;

        OutputStream fileStream = null;
        try {
            fileStream = getOutputStream(dirName, fileName);

            PrintStream pStream = new PrintStream(fileStream, true, "UTF8");
            pStream.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?> ");
            write(root, pStream, saveChildren);
            pStream.close();
            fileStream.close();
            fileStream=null;
        } catch (Exception e) {
            if (log.isFatalEnabled()) {
                log.fatal(this, e);
            }
        } finally {
            if (fileStream != null) {
                try {
                    fileStream.close();
                } catch (IOException e) {
                    if(log.isErrorEnabled())
                        log.error(this, e);
                }
            }
        }
    }

    /**
     * Saves a Persistable
     *
     * @param obj object to be saved
     */
    public void save(Persistable obj) {
        save(obj, true);
    }

    /**
     * Writes a persitent object to the print stream provided.
     *
     * @param pComp component to be written out.
     * @param out print stream to which the component is written to.
     * @param reanderChildren determine whether if to write its children.
     */
    public void write(Persistable pComp, PrintStream out, boolean reanderChildren) {
        beginEncode(pComp, out);

        if (reanderChildren) {
            encodeChildren(pComp, out);
        }
        endEncode(out);
    }

    /**
     * Returns file name for an object.
     *
     * @param bean the object.
     *
     * @return filename, not include path.
     */
    protected String getFileName(Persistable  bean) {
        return bean.getId();
    }

    /**
     * Checks if a given operation is allowed on a bean.
     * @param bean the object.
     * @param operation the operation to be checked. 
     * @return true if the operation is allowed.
     */
    protected boolean checkSecurity(Persistable bean, int operation) {
        if(log.isDebugEnabled())
            log.debug("checkSecurity" + bean);
        if (operation != READ) {
            return !isReadOnly();
        }
        return true;
    }

    protected boolean load(Object root, String fileName) {
        if(log.isDebugEnabled())
            log.debug("load");
        
        InputStream inputFile = null;
        try {

            // Step 1: create a DocumentBuilderFactory and configure it
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

            // Set namespaceAware to true to get a DOM Level 2 tree with nodes
            // containing namesapce information.  This is necessary because the
            // default value from JAXP 1.0 was defined to be false.
            dbf.setNamespaceAware(true);

            // Set the validation mode to either: no validation, DTD
            // validation, or XSD validation
            dbf.setValidating(false);

            // Step 2: create a DocumentBuilder that satisfies the constraints
            // specified by the DocumentBuilderFactory
            inputFile = getInputStream(fileName);
            if(inputFile==null) {
                return false;
            }
            /*
             String charset = "UTF-8";
             FileInputStream inputFile = new FileInputStream(fileName);
             InputStreamReader inputStream = new InputStreamReader(inputFile, charset);
             BufferedReader bufferReader = new BufferedReader(inputStream);
             */

            // Step 3: parse the input file
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse(inputFile);

            Node xmlElement =  doc.getDocumentElement();

            //if(xmlElement instanceof org.w3c.dom.traversal.
            processComponent((Persistable) root, xmlElement, false);
            return true;
        } catch (Exception e) {
            if (log.isFatalEnabled()) {
                log.fatal(this, e);
            }
            return false;
        } finally {
            if (inputFile != null) {
                try {
                    inputFile.close();
                } catch (Exception e) {
                    if(log.isErrorEnabled())
                        log.error(this, e);
                }
            }
        }
    }
    
    protected OutputStream getOutputStream(String path, String fileName) throws IOException {

        if (!(new File(path)).exists()) {
            (new File(path)).mkdirs();
        }

        File file = new File(fileName);
        File backFile = null;

        if (file.exists()) {
            backFile = new File(fileName + ".bak");

            //TODO rename to a different file name first
            //then delele, the reaname to bak file
            //if failed, rename to old one back
            //otherwise delete to old one
            
            if (backFile.exists()) {
                if (!backFile.delete()) {
                    throw new IOException("Failed to backup current file "+fileName);
                }
            }
            file.renameTo(backFile);
        }
        return new FileOutputStream(fileName);
    }
    
    protected InputStream getInputStream(String fileName) throws IOException {
        File file = new File(fileName);

        if (!file.exists()) {
            return null;
        }
        return new FileInputStream(fileName);
    }
    
    protected Persistable processComponent(Persistable parent, Node xmlElement, boolean isFacet) {
        try {
            NamedNodeMap attr = xmlElement.getAttributes();
            String compType = "";
            String compId = "";

            if (attr != null) {
                for (int i = 0; i < attr.getLength(); i++) {
                    Node node = attr.item(i);
                    String name = node.getLocalName();
                    String value = node.getNodeValue();

                    if ("type".equals(name)) {
                        compType = value;
                    } else if ("id".equals(name)) {
                        compId = value;
                    }
                }
            }

            Persistable comp = null;

            if ((parent != null) && !(parent.getId().equals(compId) && compType.equals(parent.getClass().getName()))) {
                comp =  createInstance(compId, ClassUtil.getClass(compType));
                if(comp==null) {
                    if (log.isFatalEnabled()) {
                        log.fatal("Failed to create "+compType);
                    }
                    return null;
                }
                comp.setId(compId);

                if (isFacet && parent instanceof UIComponentImpl) {
                    ((UIComponentImpl) parent).addFacet(compId, (UIComponent) comp);
                } else {
                    parent.addChild(comp);
                }                
            } else {
                comp = parent;
            }

            NodeList nodes = xmlElement.getChildNodes();

            for (int i = 0; i < nodes.getLength(); i++) {
                Node groupChild = nodes.item(i);

                if ("properties".equals(groupChild.getLocalName())) {
                    NodeList properties = groupChild.getChildNodes();

                    for (int j = 0; j < properties.getLength(); j++)
                        processProperty(comp, properties.item(j));
                }

                if ("components".equals(groupChild.getLocalName())) {
                    NodeList components = groupChild.getChildNodes();

                    for (int j = 0; j < components.getLength(); j++)
                        processChild(comp, components.item(j), false);
                }

                if ("facets".equals(groupChild.getLocalName())) {
                    NodeList components = groupChild.getChildNodes();

                    for (int j = 0; j < components.getLength(); j++)
                        processChild(comp, components.item(j), true);
                }
            }

            comp.postCreate(null);

            return comp;
        } catch (Exception e) {
            if (log.isFatalEnabled()) {
                log.fatal(this, e);
            }
        }

        return null;
    }

    private Object getComponentPropertyValue(Node property) {
        NamedNodeMap attr = property.getAttributes();
        String compValue = "";

        for (int i = 0; i < attr.getLength(); i++) {
            Node node = attr.item(i);
            String name = node.getLocalName();
            String value = node.getNodeValue();

            if ("value".equals(name)) {
                compValue = value;
            }
        }

        return compValue;
    }

    private String getLocation(Persistable root) {
        if (root instanceof Page) {
            Page page = (Page) root;
            //if application is null, we are loading the page
            if (location==null && page.getApplication() != null) { 
                return Config.getInstance().getApplicationTop() + File.separator + page.getApplication();
            } else {
                return location;
            }
        } else {
            return location;
        }
    }



    private void beginEncode(Persistable pComp, PrintStream out) {
        String type = pComp.getClass().getName();

        if ("java.lang.String".equals(type)) {
            type = null;
        }

        out.print("<component id=\"" + pComp.getId() + "\"");

        if (type != null) {
            out.print(" type=\"" + type + "\"");
        }

        out.println(">");

        Iterator iterator = com.jb.framework.util.PropUtil.getPropertyNames(pComp.getClass()).iterator();
        out.println("<properties>");

        while (iterator.hasNext()) {
            String propName = (String) iterator.next();

            if ("parent".equals(propName)) {
                continue;
            }

            if ("id".equals(propName)) {
                continue;
            }

            try {
                Object propValue = com.jb.framework.util.PropUtil.getProperty(pComp, propName);

                if (propValue == null) {
                    continue;
                }

                if (propValue instanceof String && (propValue.toString().length() == 0)) {
                    //write nothing
         //       } else if (propValue instanceof Boolean && !((Boolean) propValue).booleanValue()) {
                    // write nothing
                } else if (supportedType(propValue) && (propValue.toString() != null)) {
                    out.print("<property name=\"" + propName);

                    if (!"java.lang.String".equals(propValue.getClass().getName())) {
                        out.print("\" type=\"" + propValue.getClass().getName());
                    }

                    out.print("\"");
                    
                    out.println(" value =\"" +
                        com.jb.framework.util.XMLUtil.encodeXMLSafe(propValue.toString().trim()) + "\" />");
                        
                }
            } catch (Exception e) {
                if (log.isFatalEnabled()) {
                    log.fatal(this, e);
                }
            }
        }

        out.println("</properties>");
    }

    private Persistable createInstance(String name, Class clazz) {
        Persistable persistable = (Persistable) ClassUtil.createInstance(clazz);
        if(persistable!=null)
            persistable.setId(name);
        return persistable;  
    }

    private void encodeChildren(Persistable pComp, PrintStream out) {
        Iterator iter = pComp.getChildren().iterator();

        if (iter.hasNext()) {
            out.println("<components>\n");

            while (iter.hasNext()) {
                Object uiChild = iter.next();

                if (uiChild instanceof Persistable) {
                    Persistable ui = (Persistable) uiChild;

                    write(ui, out, true);
                }
            }

            out.println("</components>\n");
        }

        //write facets
        if (pComp instanceof UIComponent) {
            UIComponent uiComp = (UIComponent) pComp;
            Map map = uiComp.getFacets();

            if (map != null) {
                iter = map.keySet().iterator();

                if (iter.hasNext()) {
                    out.println("<facets>\n");

                    while (iter.hasNext()) {
                        Object uiChild = map.get(iter.next());

                        if (uiChild instanceof Persistable) {
                            Persistable ui = (Persistable) uiChild;
                            write(ui, out, true);
                        }
                    }

                    out.println("</facets>\n");
                }
            }
        }
    }

    //write component
    private void endEncode(PrintStream out) {
        out.println("</component>");
    }

    private void processChild(Persistable parent, Node childNode, boolean isFacet) {
        NamedNodeMap attr = childNode.getAttributes();

        if (attr == null) {
            return;
        }

        try {
            processComponent((Persistable) parent, childNode, isFacet);
        } catch (Exception e) {
            if (log.isFatalEnabled()) {
                log.fatal(this, e);
            }
        }
    }

    private void processProperty(Persistable component, Node property) {
        NamedNodeMap attr = property.getAttributes();
        String compType = "";
        String compName = "";

        if (attr == null) {
            return;
        }

        for (int i = 0; i < attr.getLength(); i++) {
            Node node = attr.item(i);
            String name = node.getLocalName();
            String value = node.getNodeValue();

            if ("name".equals(name)) {
                compName = value;
            } else if ("type".equals(name)) {
                compType = value;
            }
        }

        Object compValue = getComponentPropertyValue(property);
        setComponentProperty(component, compName, compValue);
    }

    private boolean supportedType(Object obj) {
        return obj instanceof Number || obj instanceof Date || obj instanceof Boolean || obj instanceof String ||
        obj.getClass().isPrimitive() || obj instanceof StringBuffer;
    }
}
